#!/usr/bin/python2
# -*- coding: utf-8 -*-


import threading
import time
import logging
import os
import sys

from eventlet.queue import Empty

# from Queue import Queue
from eventlet import Queue,sleep

from ryuapp.lib.hedera.mock_dep import DepGraph,DepNode,Operation
from ryuapp.lib.hedera.updatewrapper import UpdateWrapper,UpdateTracker


class FlowNodeStatus:
    def __init__(self):
        self.dep_node_waiting = None
        self.dst_waiting = None
        self.dst = None


class FlowStatus:
    def __init__(self):
        self.flow_nodes = {}
        self.path = None
        self.path_waiting = None
        self.path_waiting_dep_node = None
        self.dep_list = []


class PartialExecutor():
    """
    Partial Executor
    """
    def __init__(self, common_queue, install_queue):
        # super(Executor, self).__init__()
        assert isinstance(common_queue, Queue)
        self.__common_queue = common_queue
        assert isinstance(install_queue, Queue)
        self.__install_queue = install_queue

        self.all_status = {}  # {pair : FlowStatus}
        self.update_history = []

    def run(self):
        start_time = None
        exec_count = 0

        while True:
            print("exec_count", exec_count)
            if start_time is not None:
                print("time_count", time.time() - start_time)
            try:
                g = self.__common_queue.get(timeout=20)
            except Empty:
                print("operation number=%d" % exec_count)
                print("update time array=%s" % list(u.tracker.duration for u in self.update_history))
                print("average time=%s" % (sum(u.tracker.duration for u in self.update_history) / len(self.update_history)))
                sys.stdout.flush()
                os._exit(0)

            glist = [g]
            while not self.__common_queue.empty():
                tmp = self.__common_queue.get()
                glist.append(tmp)

            updates = []
            notification = []
            for g in glist:
                if isinstance(g, UpdateWrapper):
                    updates.append(g)
                elif isinstance(g,Operation):
                    notification.append(g)

            for op in notification:
                self.__release_one(op)

            if updates:
                for update in updates:
                    print("udpates",update)
                    self.__merge(update)
            for pair,status in self.all_status.iteritems():
                for n in status.dep_list:
                    if n.status==DepNode.INIT and len(n.acquire)==0:
                        self.__exec(status, n)
                        exec_count+=1


    def __merge(self, update):
        assert isinstance(update,UpdateWrapper)
        self.update_history.append(update)
        assert isinstance(update.update,dict)
        for pair,path in update.update.iteritems():
            if pair not in self.all_status:
                self.all_status[pair] = FlowStatus()
            flow_status = self.all_status[pair]
            assert isinstance(flow_status,FlowStatus)
            dep_list = flow_status.dep_list

            # cancel init node
            cancel_dep_node_list = [n for n in dep_list if n.status == DepNode.INIT]
            for n in cancel_dep_node_list:
                self.__remove_dependency(n)
                dep_list.remove(n)
            for n in dep_list:
                assert len(n.acquire)==0 and len(n.release)==0 and n.status == DepNode.WAIT
            count1 = len(dep_list)

            # recompute operation
            dep = self.__compute_dependency(pair,flow_status,path)
            flow_status.dep_list = dep.nodes

            #check
            tmp = [n for n in dep.nodes if n.op.path!=None]
            assert len(tmp)<3

            # maintain update tracker
            for n in cancel_dep_node_list:
                self.__update_track_dec(n)
            count = 0
            for n in dep.nodes:
                if not hasattr(n,"update_history"):
                    self.__update_track_inc(update,n)
                else:
                    count +=1
            assert count == count1
        if update.tracker.remained_op_num ==0:
            update.tracker.duration = time.time()-update.tracker.starttime
        else:
            update.tracker.duration = 0


    def __update_track_inc(self,update, dep_node):
        # assert dep_node.update_history is None
        dep_node.update_history = update
        assert isinstance(update, UpdateWrapper)
        update.tracker.remained_op_num += 1

    def __update_track_dec(self, dep_node):
        u = dep_node.update_history
        assert isinstance(u, UpdateWrapper)
        assert u.tracker.remained_op_num > 0
        u.tracker.remained_op_num -= 1
        if u.tracker.remained_op_num == 0:
            u.tracker.duration = time.time() - u.tracker.starttime
            print("duration=%f"%u.tracker.duration)


    def __exec(self,status, n):
        assert isinstance(status,FlowStatus)
        assert isinstance(n,DepNode)
        n.status = DepNode.WAIT
        if n.op.path!=None:
            status.path_waiting_dep_node = n
            status.path_waiting = n.op.path
        fs = status.flow_nodes
        if n.op.node not in fs:
            fs[n.op.node]=FlowNodeStatus()
        fns = fs[n.op.node]
        assert isinstance(fns,FlowNodeStatus)
        assert fns.dep_node_waiting is None
        fns.dep_node_waiting = n
        op=n.op
        if op.type == Operation.ADD or op.type==Operation.MOD:
            fns.dst_waiting = op.dst
            fns.dep_node_waiting = n
        elif op.type == Operation.DEL:
            fns.dst_waiting = None
            fns.dep_node_waiting = n
        else:
            raise Exception("error")
        self.__install_queue.put(n.op)
        print("exec",str(n.op))

    def __release_one(self, op):
        print("release", str(op), op)
        assert isinstance(op, Operation)
        node = op.node
        pair = op.pair
        fs = self.all_status[pair]
        assert isinstance(fs,FlowStatus)
        fns = fs.flow_nodes[node]
        assert isinstance(fns, FlowNodeStatus)
        # if fns.dep_node_waiting is None:
        #     k=1
        assert fns.dep_node_waiting.op.eq(op)
        dep_node = fns.dep_node_waiting
        dep_node.status = DepNode.EXECED

        self.__update_track_dec(dep_node)

        self.__remove_dependency(dep_node)
        assert dep_node in fs.dep_list
        fs.dep_list.remove(dep_node)
        fns.dep_node_waiting=None
        fns.dst = fns.dst_waiting
        fns.dst_waiting = None
        if fs.path_waiting_dep_node==dep_node:
            fs.path =fs.path_waiting
            fs.path_waiting_dep_node = None
            fs.path_waiting = None



    def __remove_dependency(self,n):
        for nr in n.release:
            if n not in nr.acquire:
                raise Exception("not in nr.acquire")
            nr.acquire.remove(n)
        for na in n.acquire:
            if n not in na.release:
                raise Exception("not in na.release")
            na.release.remove(n)

    def __add_dependency(self, r, a):
        r.release.append(a)
        a.acquire.append(r)


    def __compute_dependency(self, pair, flow_status, target_path):
        """
        :param pair:
        :param flow_status:
        :param target_path: (1001,2002,3003)
        :return:
        """
        assert target_path is not None
        assert isinstance(flow_status,FlowStatus)
        dep = DepGraph()

        node_path2 = target_path
        path2 = [(node_path2[i], node_path2[i + 1]) for i in range(len(node_path2) - 1)]
        path2_map = dict(path2)

        waiting_map = {}
        new_map = {}

        for node,status in flow_status.flow_nodes.iteritems():
            assert isinstance(status,FlowNodeStatus)
            if status.dep_node_waiting:
                waiting_map[node]=status.dep_node_waiting

        # check
        tmp = waiting_map.values()
        for i in flow_status.dep_list:
            assert i in tmp
        for i in tmp:
            assert i in flow_status.dep_list


        for node in path2_map:
            flow_status.flow_nodes.setdefault(node,FlowNodeStatus())
        for node,status in flow_status.flow_nodes.iteritems():
            assert isinstance(status, FlowNodeStatus)
            target_dst = path2_map.get(node)
            n = self.__diff_dep_node(pair, node, status, target_dst)
            if n:
                new_map[node]=n

        if flow_status.path_waiting:
            node_path1 = flow_status.path_waiting
        else:
            node_path1 = flow_status.path


        if node_path1==node_path2:
            dep.nodes = list(flow_status.dep_list)
            dep.nodes.extend(new_map.values())
            return dep

        path1 = None
        if node_path1:
            path1 = [(node_path1[i],node_path1[i+1]) for i in range(len(node_path1)-1)]

        if path1 == None:
            first_install_node = path2[0][0]
            first_install = new_map[first_install_node]
            for link in path2[1:]:
                n = link[0]
                if n in waiting_map:
                    self.__add_dependency(waiting_map[n], first_install)
                if n in new_map:
                    self.__add_dependency(new_map[n], first_install)
            first_install.op.path = target_path
            dep.nodes = list(flow_status.dep_list)
            dep.nodes.extend(new_map.values())
            return dep
        else:
            path1_map = dict(path1)
            op_map = {}
            for n,p in path2_map.items():
                if n in path1_map:
                    if p!=path1_map[n]:
                        op_map[n]= Operation.MOD
                else:
                    op_map[n] = Operation.ADD
            for n,p in path1_map.items():
                if n not in path2_map:
                    op_map[n]= Operation.DEL
            firstmod = None
            for n,p in path2:
                if n not in op_map:
                    continue
                op = op_map[n]
                if op == Operation.MOD:
                    assert firstmod is None # TODO
                    if firstmod is not None:
                        self.__add_both_dependency(waiting_map,new_map,firstmod, n)
                    firstmod = n
                elif op == Operation.ADD:
                    if firstmod is not None:
                        self.__add_both_dependency(waiting_map,new_map,n, firstmod)
                else:
                    raise Exception("unexpected operation")
            firstmod = None
            for n,p in path1:
                if n not in op_map:
                    continue
                op = op_map[n]
                if op == Operation.MOD:
                    assert firstmod is None  # TODO
                    firstmod = n
                elif op == Operation.DEL:
                    if firstmod is not None:
                        self.__add_both_dependency(waiting_map,new_map,firstmod,n)
                else:
                    raise Exception("unexpected operation")

            assert firstmod in new_map
            c_node = new_map[firstmod]
            c_node.op.path = target_path

            if flow_status.path_waiting:
                # check
                tmp = [n for n in flow_status.dep_list if n.op.path]
                assert len(tmp)==1
                assert tmp[0]==flow_status.path_waiting_dep_node
                self.__add_dependency(flow_status.path_waiting_dep_node, c_node)

            dep.nodes = list(flow_status.dep_list)
            dep.nodes.extend(new_map.values())
            return dep

    def __add_both_dependency(self, waiting_map, new_map, r, a):
        if r in waiting_map:
            if a in waiting_map:
                pass
                # raise Exception("error")
            if a in new_map:
                self.__add_dependency(waiting_map[r],new_map[a])
        if r in new_map:
            if a in new_map:
                self.__add_dependency(new_map[r],new_map[a])
            elif a in waiting_map:
                raise Exception("error 2")


    def __diff_dep_node(self,pair, node, status, target_dst):
        assert isinstance(status, FlowNodeStatus)
        if status.dep_node_waiting:
            op = self.__diff_operation(pair, node, status.dst_waiting, target_dst)
            if op:
                f = DepNode(op)
                self.__add_dependency(status.dep_node_waiting,f)
                return f
        else:
            op = self.__diff_operation(pair, node, status.dst, target_dst)
            if op:
                return DepNode(op)
        return None

    def __diff_operation(self, pair, node, dst, target_dst):
        if dst==target_dst:
            return None
        if dst==None:
            return Operation(node,Operation.ADD,pair,target_dst)
        elif target_dst==None:
            return Operation(node,Operation.DEL,pair,None)
        else:
            return Operation(node,Operation.MOD,pair,target_dst)
